package se.nikmel.AnDisper.DisperServer;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.io.StringReader;
import java.io.StringWriter;
import java.net.Socket;
import java.net.SocketException;
import java.net.UnknownHostException;

import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;

import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.DefaultHandler;
import org.xmlpull.v1.XmlSerializer;

import se.nikmel.AnDisper.DisperServer.DisperInterface.E_DISPER_PROTOCOL;
import android.util.Log;
import android.util.Xml;

/* ********* Socket-klassen ************************************ */

/*
 * This class now provides the low level communication interface with the server.
 * The class is responsible for maintaining a socket to the server and to send commands and 
 * propagate replies. No decoding takes place in this class; thi is left to the DisperInterface class.
 * This class should be made private in the package as it does not provide any useful external interface.
 * 
 *  The class is responsible for the messages PING_REQUEST, PING_RESPONSE and CLOSE_CONNECTION
 * 
 * OLD PROTOCOL
 * Protocol CLIENT -> SERVER "get_config" -> Available screens
 * "set | <disper options syntax>" -> Exec "disper " <response>
 * 
 * 
 */

class DisperConnection {
	private static final String TAG = "DisperConnection";
	private static final int SOCKET_TIMEOUT = 1500; // in ms
	private static final int SOCKET_PING_TIMEOUT = 750; // in ms
	private static final int SOCKET_PORT = 52001;

	private int m_iSocketNr;
	private Long m_lLastPing_Tick;
	private String m_sServerIP;
	private Socket m_sckConnection;

	private SAXParserFactory m_oXMLParserFactory;
	private SAXParser m_oXMLParser;

	public DisperConnection(String p_sServerIP) {
		this(p_sServerIP, SOCKET_PORT);
	}

	public DisperConnection(String p_sServerIP, Integer p_iSocketNr) {
		if (p_iSocketNr == null)
			m_iSocketNr = SOCKET_PORT;
		else
			m_iSocketNr = p_iSocketNr;

		m_sServerIP = p_sServerIP;
		m_lLastPing_Tick = null;
	}

	public void close() {
		if (m_sckConnection == null)
			return;

		try {
			String sCloseRequest = createMessage_Close();
			Log.v(TAG, "(II) Sending close request to server");
			send(sCloseRequest, m_sckConnection);
			m_sckConnection = null;

		} catch (IllegalArgumentException e) {
			Log.e(TAG, "Failed to close server", e);
		} catch (IllegalStateException e) {
			Log.e(TAG, "Failed to close server", e);
		} catch (IOException e) {
			Log.e(TAG, "Failed to close server", e);
		}
	}

	public String getServerIP() {
		return m_sServerIP;
	}

	public void setServerIP(String p_sIPAddress) {
		close();
		m_sckConnection = null;
		m_sServerIP = p_sIPAddress;
	}

	public boolean isConnected() {
		// Just get a connection. If it succeeds, then we are connected;
		// exception -> not...
		try {
			Log.v(TAG, "(II) Socket says it is connected? "
					+ getConnection().isConnected());
			return true;

		} catch (UnknownHostException e) {
			Log.e(TAG, "(EE) Unknown host when checking if host is connected",
					e);
		} catch (IOException e) {
			Log.e(TAG, "(EE) IO error when checking if host is connected", e);
		}
		return false;
	}

	public void send(String p_sMessage) {
		try {
			send(p_sMessage, getConnection());
		} catch (UnknownHostException e) {
			Log.e(TAG, "(EE) UnknownhostException caught in sendWithReply(): "
					+ e.toString());
			// this.close();
		} catch (IOException e) {
			Log.e(TAG,
					"(EE) IOException caught in sendWithReply(): "
							+ e.toString());
			// this.close();
		}
	}

	private void send(String p_sMessage, Socket p_sckConn) {
		try {
			Socket s = p_sckConn;

			// Outgoing stream redirect to socket
			OutputStream out = s.getOutputStream();
			PrintWriter output = new PrintWriter(out, true); // true=Autoflush
			output.println(p_sMessage);
			output.flush();

		} catch (UnknownHostException e) {
			Log.e(TAG,
					"(EE) Unknown host exception caught in send():\n"
							+ e.getMessage());
			return;
		} catch (IOException e) {
			Log.e(TAG, "(EE) IOException caught in send():\n" + e.getMessage());
			return;
		} catch (Exception e) {
			Log.e(TAG,
					"(EE) Unanticipated exception caught in send():\n"
							+ e.getMessage());
			return;
		}
	}

	public String sendWithReply(String p_sMessage) {
		try {

			return sendWithReply(p_sMessage, getConnection());

		} catch (UnknownHostException e) {
			Log.e(TAG, "(EE) UnknownhostException caught in sendWithReply(): "
					+ e.toString());
			// this.close();
		} catch (IOException e) {
			Log.e(TAG,
					"(EE) IOException caught in sendWithReply(): "
							+ e.toString());
			// this.close();
		}
		return null;
	}

	/* ******************** PRIVATE METHODS *************** */

	private XMLReader getXMLReader() throws ParserConfigurationException,
			SAXException {
		if (m_oXMLParserFactory == null) {
			m_oXMLParserFactory = SAXParserFactory.newInstance();
			m_oXMLParser = m_oXMLParserFactory.newSAXParser();
		}

		return m_oXMLParser.getXMLReader();
	}

	private Socket getConnection() throws UnknownHostException, IOException {
		boolean bCreateNewConnection = false;

		if (m_sckConnection == null)
			bCreateNewConnection = true;
		else {
			// Ensure server is responding
			bCreateNewConnection = !pingServer();
		}

		if (bCreateNewConnection) {
			m_sckConnection = new Socket(m_sServerIP, m_iSocketNr);
			Log.v(TAG,
					"(II) SO_REUSEADDR: " + m_sckConnection.getReuseAddress());
			m_sckConnection.setReuseAddress(true);
			m_sckConnection.setSoTimeout(SOCKET_TIMEOUT);

			Log.v(TAG,
					"(II) SO_RecieveBufferSize: "
							+ m_sckConnection.getReceiveBufferSize());
		}

		return m_sckConnection;
	}

	private String sendWithReply(String p_sMessage, Socket p_sckConn) {
		String sResponse = null;
		Log.v(TAG, "(II) Sending message with request for reply:\n"
				+ p_sMessage);

		try {
			Socket s = p_sckConn;

			// Outgoing stream redirect to socket
			OutputStream out = s.getOutputStream();

			// Skicka begäran
			PrintWriter output = new PrintWriter(out); // true=Autoflush
			output.println(p_sMessage);
			output.flush();
			Log.v(TAG, "(II) DisperConnetion sent message and awaits reply");

			// Ta emot svaret
			BufferedReader input = new BufferedReader(new InputStreamReader(
					s.getInputStream()));

			StringBuilder sbResponse = new StringBuilder();
			String sTmp = input.readLine();
			if (sTmp == null) {
				Log.v(TAG, "(II) Got a null reply from server");
				sTmp = "";
			} else
				Log.v(TAG, "(II) Got a reply from server: " + sTmp);
			sbResponse.append(sTmp);
			while (input.ready()) {
				Log.v(TAG, "(II) About to receive more data!");
				sTmp = input.readLine();
				if (sTmp == null) {
					Log.v(TAG, "(II) Got another null reply from server");
					sTmp = "";
				} else
					Log.v(TAG, "(II) Got a another reply from server: " + sTmp);
				sbResponse.append(sTmp);
			}
			if (sbResponse.length() == 0) {
				Log.v(TAG, "(II) DisperConnetion received a zero length reply!");
				sResponse = null;
			} else {
				sResponse = sbResponse.toString();
				Log.v(TAG, "(II) DisperConnetion received a reply:\n"
						+ sResponse);
			}

		} catch (UnknownHostException e) {
			Log.e(TAG,
					"(EE) Unknown host exception caught in sendWithReply(): "
							+ e.getMessage());
			// this.close();
			return null;

		} catch (IOException e) {
			Log.e(TAG,
					"(EE) IOException caught in sendWithReply(String, Socket): "
							+ e.toString());
			// this.close();
			return null;

		} catch (Exception e) {
			Log.e(TAG,
					"(EE) Unanticipated exception caught in sendWithReply(): "
							+ e.getMessage());
			// this.close();
			return null;
		}

		return sResponse;
	}

	private boolean pingServer() {
		if (m_sckConnection == null)
			return false;

		// Only ping once every SOKET_TIMEOUT seconds, assume server is still
		// alive
		if (m_lLastPing_Tick != null
				&& (System.currentTimeMillis() - m_lLastPing_Tick) < SOCKET_TIMEOUT){
			Log.v(TAG,"(II) Skipping PING!");
			return true;
		}

		try {
			String sPingRequest = createMessage_Ping();
			m_sckConnection.setSoTimeout(SOCKET_PING_TIMEOUT); // Shorter
																// ping...
			String sPingResponse = sendWithReply(sPingRequest, m_sckConnection);
			Log.v(TAG, "(II) Server responded to PING with:" + sPingResponse);
			m_sckConnection.setSoTimeout(SOCKET_TIMEOUT); // Restore timeout
			if (parseReply_Ping(sPingResponse)) {
				m_lLastPing_Tick = new Long(System.currentTimeMillis());
				return true;
			} else
				return false;

		} catch (IllegalArgumentException e) {
			Log.e(TAG, "Failed to ping server", e);
		} catch (IllegalStateException e) {
			Log.e(TAG, "Failed to ping server", e);
		} catch (IOException e) {
			Log.e(TAG, "Failed to ping server", e);
		}
		Log.v(TAG, "(II) Server FAILED to respond to PING");

		try {
			m_sckConnection.setSoTimeout(SOCKET_TIMEOUT);
		} catch (SocketException e) {
			Log.v(TAG, "(WW) Failed resetting timeout for socket after ping!");
		} // Shorter ping...
		return false;
	}

	// Creates a CLOSE_CONNECTION message
	private String createMessage_Close() throws IllegalArgumentException,
			IllegalStateException, IOException {
		XmlSerializer xmlCommand = Xml.newSerializer();
		StringWriter strWriter = new StringWriter();
		xmlCommand.setOutput(strWriter);

		xmlCommand.startDocument("UTF-8", true);
		xmlCommand.startTag("", DisperInterface.XML_MESSAGE_TAG);
		xmlCommand.startTag("", DisperInterface.XML_COMMAND_TAG);
		xmlCommand.text(E_DISPER_PROTOCOL.CLOSE_CONNECTION.toString());
		xmlCommand.endDocument();

		return strWriter.toString();
	}

	// Creates a PING_REQUEST message
	private String createMessage_Ping() throws IllegalArgumentException,
			IllegalStateException, IOException {
		XmlSerializer xmlCommand = Xml.newSerializer();
		StringWriter strWriter = new StringWriter();
		xmlCommand.setOutput(strWriter);

		xmlCommand.startDocument("UTF-8", true);
		xmlCommand.startTag("", DisperInterface.XML_MESSAGE_TAG);
		xmlCommand.startTag("", DisperInterface.XML_COMMAND_TAG);
		xmlCommand.text(E_DISPER_PROTOCOL.PING_REQUEST.toString());
		xmlCommand.endDocument();

		return strWriter.toString();
	}

	// Parses a reply from a PING_REQUEST message and returns true if ok
	private boolean parseReply_Ping(String p_sReply) {
		if (p_sReply == null)
			return false;
		if (p_sReply.compareTo("null") == 0)
			Log.v(TAG, "(WW) Ping apparently responded with the string null!");

		try {
			XMLReader xr = getXMLReader();

			_PingResponseParser oHandler = new _PingResponseParser();
			xr.setContentHandler(oHandler);

			InputSource is = new InputSource();
			is.setCharacterStream(new StringReader(p_sReply));
			xr.parse(is);

			return oHandler.isPingResponse();

		} catch (ParserConfigurationException pce) {
			Log.e("SAX XML", "sax parse error", pce);
		} catch (SAXException se) {
			Log.e("SAX XML", "sax error when parsing ping reply:\n" + p_sReply,
					se);
		} catch (IOException ioe) {
			Log.e("SAX XML", "sax parse io error", ioe);
		}

		return false;
	}

	private class _PingResponseParser extends DefaultHandler {

		// booleans that check whether it's in a specific tag or not
		private boolean m_bInReponseTag;

		private String m_sInResponseToTag;
		private String m_sResponse;

		/**
		 * Returns whether we got a valid ping from the server
		 * 
		 * @return
		 */
		public boolean isPingResponse() {
			return (m_sInResponseToTag.equals(E_DISPER_PROTOCOL.PING_REQUEST
					.toString()) && m_sResponse
					.equals(E_DISPER_PROTOCOL.PING_RESPONSE.toString()));
		}

		/* ********************* PARSER CALLBACKS ***************** */
		@Override
		public void startDocument() throws SAXException {
			m_bInReponseTag = false;
		}

		@Override
		public void startElement(String uri, String localName, String qName,
				org.xml.sax.Attributes attributes) throws SAXException {

			if (localName.equals(DisperInterface.XML_RESPONSE_TAG)) {
				m_sInResponseToTag = attributes
						.getValue(DisperInterface.XML_RESPONSE_COMMAND_ATTR);
				m_bInReponseTag = true;
			}
		}

		@Override
		public void endElement(String namespaceURI, String localName,
				String qName) throws SAXException {
			m_bInReponseTag = false;
		}

		@Override
		public void characters(char ch[], int start, int length) {
			String sText = new String(ch, start, length);
			sText = sText.trim();

			if (m_bInReponseTag) {
				m_sResponse = sText;
			}
		}
	}
}
